﻿//==============================================================================
// File: ChromeWindow.cs
// Created: 2010-05-28
// Author: Piotr Włodek, Arek Świerczek
//==============================================================================
// This file is a part of MAMMOTH project.
// Copyright (C) 2010 AGH University of Science and Technology, Krakow.
// https://caribou.iisg.agh.edu.pl/trac/mammoth
//
// Mammoth is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// 
// Mammoth is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Lesser General Public License for more details.
// 
// You should have received a copy of the GNU Lesser General Public License
// along with Mammoth. If not, see http://www.gnu.org/licenses/.
//==============================================================================

using System;
using System.Globalization;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Interop;
using System.Windows.Media;

namespace WpfRcp.Controls
{
    [TemplatePart(Name = PART_ChromeThumb, Type = typeof(Thumb))]
    [TemplatePart(Name = PART_Minimize, Type = typeof(Button))]
    [TemplatePart(Name = PART_Maximize, Type = typeof(Button))]
    [TemplatePart(Name = PART_Close, Type = typeof(Button))]
    public class ChromeWindow : Window
    {
        // Constants
        private const string PART_ChromeThumb = "PART_ChromeThumb";
        private const string PART_Minimize = "PART_Minimize";
        private const string PART_Maximize = "PART_Maximize";
        private const string PART_Close = "PART_Close";

        // Class fields
        private Point CurrentPoint { get; set; }
        private WindowState PreviousFullScreenWindowState { get; set; }
        private ResizeMode PreviousFullScreenResizeMode { get; set; }
        private WindowStyle PreviousFullScreenWindowStyle { get; set; }
        private ChromeWindowStyle PreviousFullScreenChromeWindowStyle { get; set; }
        private double PreviousTop { get; set; }
        private double PreviousLeft { get; set; }

        static ChromeWindow()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(ChromeWindow), new FrameworkPropertyMetadata(typeof(ChromeWindow)));
            WindowStateProperty.OverrideMetadata(typeof(ChromeWindow), new FrameworkPropertyMetadata(OnWindowStateChangedHandler, OnWindowStateCoerceValueHandler));

            EventManager.RegisterClassHandler(typeof(ChromeWindow), Thumb.DragStartedEvent, new DragStartedEventHandler(OnDragStart));
            EventManager.RegisterClassHandler(typeof(ChromeWindow), Thumb.DragCompletedEvent, new DragCompletedEventHandler(OnDragCompleted));
            EventManager.RegisterClassHandler(typeof(ChromeWindow), Thumb.DragDeltaEvent, new DragDeltaEventHandler(OnDragDelta));
        }

        public ChromeWindow()
        {
            // Make window borderless, we will provide our own borders, never change these properties
            EnableCustomChrome();
            // Avoid situation when main window loses focus when child window is being closed.
            Closing += OnDeactivated;
        }

        private void OnDeactivated(object sender, EventArgs e)
        {
            if(Owner != null)
            {
                Owner.Activate();
            }
        }

        #region Public Properties

        public static readonly DependencyProperty IsBodyMoveableProperty =
            DependencyProperty.Register("IsBodyMoveable", typeof(bool), typeof(ChromeWindow),
                                        new UIPropertyMetadata(false));

        public bool IsBodyMoveable
        {
            get { return (bool)GetValue(IsBodyMoveableProperty); }
            set { SetValue(IsBodyMoveableProperty, value); }
        }

        public static readonly DependencyProperty BorderCornerRadiusProperty =
            DependencyProperty.Register("BorderCornerRadius", typeof(CornerRadius), typeof(ChromeWindow),
                                        new UIPropertyMetadata(new CornerRadius(0.0)));

        public CornerRadius BorderCornerRadius
        {
            get { return (CornerRadius)GetValue(BorderCornerRadiusProperty); }
            set { SetValue(BorderCornerRadiusProperty, value); }
        }

        public static readonly DependencyProperty IsFullScreenProperty =
            DependencyProperty.Register("IsFullScreen", typeof(bool), typeof(ChromeWindow),
                                        new UIPropertyMetadata(false, OnIsFullScreenChanged));

        public bool IsFullScreen
        {
            get { return (bool)GetValue(IsFullScreenProperty); }
            set { SetValue(IsFullScreenProperty, value); }
        }

        public static readonly DependencyProperty ChromeWindowStyleProperty =
            DependencyProperty.Register("ChromeWindowStyle", typeof(ChromeWindowStyle), typeof(ChromeWindow),
                                        new UIPropertyMetadata(ChromeWindowStyle.Normal));

        public ChromeWindowStyle ChromeWindowStyle
        {
            get { return (ChromeWindowStyle)GetValue(ChromeWindowStyleProperty); }
            set { SetValue(ChromeWindowStyleProperty, value); }
        }

        public static readonly DependencyProperty IsCloseButtonVisibleProperty =
            DependencyProperty.Register("IsCloseButtonVisible", typeof(bool), typeof(ChromeWindow), new UIPropertyMetadata(true));


        public bool IsCloseButtonVisible
        {
            get { return (bool)GetValue(IsCloseButtonVisibleProperty); }
            set { SetValue(IsCloseButtonVisibleProperty, value); }
        }

        #endregion

        #region Enable / Disable Custom Chrome

        public void EnableCustomChrome()
        {
            WindowStyle = WindowStyle.None;
            ResizeMode = ResizeMode.NoResize;
        }

        public void DisableCustomChrome()
        {
            WindowStyle = WindowStyle.SingleBorderWindow;
            ResizeMode = ResizeMode.CanResize;
            ChromeWindowStyle = ChromeWindowStyle.NoChrome;
        }

        public bool IsCustomChromeEnabled()
        {
            return WindowStyle == WindowStyle.None &&
                   ResizeMode == ResizeMode.NoResize;
        }

        #endregion

        #region FullScreen Implementation

        private static void OnIsFullScreenChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var window = d as ChromeWindow;
            if (window != null)
            {
                if ((bool)e.NewValue)
                {
                    ShowFullScreen(window);
                }
                else
                {
                    ExitFullScreen(window);
                }
            }
        }

        private static void ShowFullScreen(ChromeWindow window)
        {
            // Save previous values
            window.PreviousFullScreenWindowState = window.WindowState;
            window.PreviousFullScreenResizeMode = window.ResizeMode;
            window.PreviousFullScreenWindowStyle = window.WindowStyle;
            window.PreviousFullScreenChromeWindowStyle = window.ChromeWindowStyle;

            window.WindowStyle = WindowStyle.None;
            window.ResizeMode = ResizeMode.NoResize;

            // Hide chrome
            window.ChromeWindowStyle = ChromeWindowStyle.NoChrome;

            if (window.IsCustomChromeEnabled())
            {
                // Show taskbar when maximized
                ShowTaskbarWhenMaximized(window, false);
            }

            if (window.WindowState == WindowState.Maximized)
                window.WindowState = WindowState.Normal;

            // Maximize the window
            window.WindowState = WindowState.Maximized;
        }

        private static void ExitFullScreen(ChromeWindow window)
        {
            // Chrome is now visible
            window.ChromeWindowStyle = window.PreviousFullScreenChromeWindowStyle;
            window.ResizeMode = window.PreviousFullScreenResizeMode;
            window.WindowStyle = window.PreviousFullScreenWindowStyle;

            window.WindowState = WindowState.Normal;
            window.WindowState = window.PreviousFullScreenWindowState;
        }

        #endregion

        #region Mouse Drag

        protected override void OnMouseDown(MouseButtonEventArgs e)
        {
            if (IsBodyMoveable && e.LeftButton == MouseButtonState.Pressed && Keyboard.IsKeyDown(Key.LeftCtrl))
            {
                Point p = e.GetPosition(this);
                CurrentPoint = p;
            }

            base.OnMouseDown(e);
        }

        protected override void OnMouseMove(MouseEventArgs e)
        {
            if (IsBodyMoveable && e.LeftButton == MouseButtonState.Pressed && Keyboard.IsKeyDown(Key.LeftCtrl))
            {
                Point p = e.GetPosition(this);
                Left += p.X - CurrentPoint.X;
                Top += p.Y - CurrentPoint.Y;
            }

            base.OnMouseMove(e);
        }

        private static void OnDragStart(object sender, DragStartedEventArgs e)
        {
            var window = sender as ChromeWindow;
            var thumb = e.OriginalSource as Thumb;

            if (window != null && thumb != null)
            {
                e.Handled = true;
            }
        }

        private static void OnDragCompleted(object sender, DragCompletedEventArgs e)
        {
            var window = sender as ChromeWindow;
            var thumb = e.OriginalSource as Thumb;

            if (window != null && thumb != null)
            {
                e.Handled = true;
            }
        }

        private static void OnDragDelta(object sender, DragDeltaEventArgs e)
        {
            var window = sender as ChromeWindow;
            var thumb = e.OriginalSource as Thumb;

            if (window != null && thumb != null)
            {
                if (thumb.Name == PART_ChromeThumb)
                {
                    window.OnHeaderDragDelta(e.HorizontalChange, e.VerticalChange);
                }
                else if (thumb.Tag is ResizeEdges)
                {
                    window.OnResizeDragDelta(e.HorizontalChange, e.VerticalChange, thumb);
                }

                e.Handled = true;
            }
        }

        #endregion

        #region Chrome Buttons

        private void OnThumbMouseDoubleClick(object sender, MouseButtonEventArgs e)
        {
            if (ChromeWindowStyle == ChromeWindowStyle.Normal)
            {
                // Maximize or minimize
                IsMaximized = !IsMaximized;
            }
        }

        private void OnMinimizeClick(object sender, RoutedEventArgs e)
        {
            WindowState = WindowState.Minimized;
        }

        private void OnMaximizeClick(object sender, RoutedEventArgs e)
        {
            // Maximize or minimize
            IsMaximized = !IsMaximized;
        }

        private void OnCloseClick(object sender, RoutedEventArgs e)
        {
            Close();
        }

        #endregion

        #region Maximize / Minimize Window

        public bool IsMaximized
        {
            get { return WindowState == WindowState.Maximized; }
            set
            {
                WindowState = value ? WindowState.Maximized : WindowState.Normal;
            }
        }

        private static object OnWindowStateCoerceValueHandler(DependencyObject d, object baseValue)
        {
            var chromeWindow = (ChromeWindow)d;

            if (chromeWindow.IsCustomChromeEnabled() && (WindowState)baseValue == WindowState.Maximized)
            {
                chromeWindow.PreviousTop = chromeWindow.Top;
                chromeWindow.PreviousLeft = chromeWindow.Left;

                var parent = chromeWindow.Owner as ChromeWindow;
                if (parent != null && parent.IsFullScreen)
                {
                    // Do not show taskbar if parent window is on fullscreen
                    ShowTaskbarWhenMaximized(chromeWindow, false);
                }
                else
                {
                    // Show taskbar when maximized
                    ShowTaskbarWhenMaximized(chromeWindow, !chromeWindow.IsFullScreen);
                }
            }

            return baseValue;
        }

        private static void OnWindowStateChangedHandler(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var chromeWindow = (ChromeWindow)d;
            if (chromeWindow.IsCustomChromeEnabled() &&
                (WindowState)e.NewValue != WindowState.Maximized)
            {
                chromeWindow.Top = chromeWindow.PreviousTop;
                chromeWindow.Left = chromeWindow.PreviousLeft;

                chromeWindow.MaxWidth = double.PositiveInfinity;
                chromeWindow.MaxHeight = double.PositiveInfinity;
            }
        }

        #endregion

        #region Window Resize And Drag

        private static bool ContainsEdge(ResizeEdges target, ResizeEdges test)
        {
            return ((int)target & (int)test) != 0;
        }

        private void OnHeaderDragDelta(double horizontalChange, double verticalChange)
        {
            Left += horizontalChange;
            Top += verticalChange;
        }

        private void OnResizeDragDelta(double horizontalChange, double verticalChange, Thumb hitThumb)
        {
            var edges = (ResizeEdges)hitThumb.Tag;

            switch (edges)
            {
                case ResizeEdges.Left:
                case ResizeEdges.Right:
                    verticalChange = 0.0;
                    break;

                case ResizeEdges.Top:
                case ResizeEdges.Bottom:
                    horizontalChange = 0.0;
                    break;
            }

            var floatBounds = GetElementBounds(this);

            if (ContainsEdge(edges, ResizeEdges.Left))
            {
                floatBounds.Width = Math.Max(MinWidth, floatBounds.Width - horizontalChange);
                if (MinWidth != floatBounds.Width)
                    floatBounds.X += horizontalChange;
            }
            else if (ContainsEdge(edges, ResizeEdges.Right))
            {
                floatBounds.Width = Math.Max(MinWidth, floatBounds.Width + horizontalChange);
            }

            if (ContainsEdge(edges, ResizeEdges.Top))
            {
                floatBounds.Height = Math.Max(MinHeight, floatBounds.Height - verticalChange);
                if (MinHeight != floatBounds.Height)
                    floatBounds.Y += verticalChange;
            }
            else if (ContainsEdge(edges, ResizeEdges.Bottom))
            {
                floatBounds.Height = Math.Max(MinHeight, floatBounds.Height + verticalChange);
            }

            // Apply
            Width = floatBounds.Width;
            Height = floatBounds.Height;
            Left = floatBounds.X;
            Top = floatBounds.Y;
            
        }

        private static Rect GetElementBounds(FrameworkElement element)
        {
            var left = Canvas.GetLeft(element);
            if (double.IsNaN(left)) left = 0.0;

            var top = Canvas.GetTop(element);
            if (double.IsNaN(top)) top = 0.0;

            var width = element.Width;
            if (double.IsNaN(width)) width = element.ActualWidth;

            var height = element.Height;
            if (double.IsNaN(height)) height = element.ActualHeight;

            return new Rect(left, top, width, height);
        }

        #endregion

        #region Taskbar Helpers

        // Nearest monitor to window
        private const int MONITOR_DEFAULTTONEAREST = 2;

        // To get a handle to the specified monitor
        [DllImport("user32.dll")]
        private static extern IntPtr MonitorFromWindow(IntPtr hwnd, int dwFlags);

        // To get the working area of the specified monitor
        [DllImport("user32.dll")]
        private static extern bool GetMonitorInfo(HandleRef hmonitor, [In, Out] MONITORINFOEX monitorInfo);

        // Rectangle (used by MONITORINFOEX)
        [StructLayout(LayoutKind.Sequential)]
        private struct RECT
        {
            public int Left;
            public int Top;
            public int Right;
            public int Bottom;
        }

        #pragma warning disable 219
        #pragma warning disable 649
        #pragma warning disable 169

        // Monitor information (used by GetMonitorInfo())
        [StructLayout(LayoutKind.Sequential)]
        private class MONITORINFOEX
        {
            public int cbSize;
            public RECT rcMonitor; // Total area
            public RECT rcWork; // Working area
            public int dwFlags;

            [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x20)]
            public char[] szDevice;
        }

        #pragma warning restore 219
        #pragma warning restore 649
        #pragma warning restore 169

        private static void ShowTaskbarWhenMaximized(ChromeWindow window, bool showTaskbar)
        {
            if (showTaskbar)
            {
                // Get handle for nearest monitor to this window
                WindowInteropHelper wih = new WindowInteropHelper(window);
                if (wih.Handle == IntPtr.Zero)
                    throw new ApplicationException("Cannot maximize window before it is shown.");

                IntPtr hMonitor = MonitorFromWindow(wih.Handle, MONITOR_DEFAULTTONEAREST);

                // Get monitor info
                MONITORINFOEX monitorInfo = new MONITORINFOEX();
                monitorInfo.cbSize = Marshal.SizeOf(monitorInfo);
                GetMonitorInfo(new HandleRef(window, hMonitor), monitorInfo);

                // Create working area dimensions, converted to DPI-independent values
                HwndSource source = HwndSource.FromHwnd(wih.Handle);
                if (source == null) return; // Should never be null
                if (source.CompositionTarget == null) return; // Should never be null
                Matrix matrix = source.CompositionTarget.TransformFromDevice;
                RECT workingArea = monitorInfo.rcWork;

                Point rightBottomPoint =
                    matrix.Transform(
                        new Point(
                            workingArea.Right - workingArea.Left,
                            workingArea.Bottom - workingArea.Top));

                Point topLeftPoint =
                    matrix.Transform(
                        new Point(workingArea.Left, workingArea.Top));

                // Maximize the window to the device-independent working area.
                // NOTE - window state must be set to Maximized as this adds certain
                // maximized behaviors eg you can't move a window while it is maximized,
                // such as by calling Window.DragMove
                window.Top = topLeftPoint.Y;
                window.Left = topLeftPoint.X;
                window.MaxWidth = rightBottomPoint.X;
                window.MaxHeight = rightBottomPoint.Y;
            }
            else
            {
                window.MaxWidth = double.PositiveInfinity;
                window.MaxHeight = double.PositiveInfinity;
            }
        }

        #endregion

        #region Overrides

        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();

            Button closeButton = GetTemplateChild(PART_Close) as Button;
            if (closeButton != null)
            {
                closeButton.Click += OnCloseClick;
            }

            Button minimizeButton = GetTemplateChild(PART_Minimize) as Button;
            if (minimizeButton != null)
            {
                minimizeButton.Click += OnMinimizeClick;
            }

            Button maximizeButton = GetTemplateChild(PART_Maximize) as Button;
            if (maximizeButton != null)
            {
                maximizeButton.Click += OnMaximizeClick;
            }

            Thumb thumb = GetTemplateChild(PART_ChromeThumb) as Thumb;
            if (thumb != null)
            {
                thumb.MouseDoubleClick += OnThumbMouseDoubleClick;
            }
        }

        #endregion
    }

    [Flags]
    public enum ResizeEdges
    {
        Left = 1,
        Top = 2,
        Right = 4,
        Bottom = 8,
        TopLeft = Top + Left,
        TopRight = Top + Right,
        BottomLeft = Bottom + Left,
        BottomRight = Bottom + Right
    }

    public enum ChromeWindowStyle
    {
        Normal,
        Single,
        None,
        NoChrome
    }

    public class ChromeWindowUpperCornerRadiusValueConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            var original = (CornerRadius) value;
            return new CornerRadius(original.TopLeft, original.TopRight, 0.0, 0.0);
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotSupportedException();
        }
    }
}